package com.google.code.wicket.auth.component.value.resolver.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;

/**
 * An {@link AnnotationValueRetrievalStrategy} that returns a single value from
 * a supplied annotation.
 * <p>
 * <code><pre>
 * &#64;Retention(RetentionPolicy.RUNTIME)
 * &#64;Target(ElementType.TYPE)
 * public &#64;interface MyAnnotation {
 *     String value() default "hello";
 *     String other() default "world";
 * }
 * 
 * &#64;MyAnnotation
 * public class MyClass1{
 *  // ...
 * }
 * 
 * &#64;MyAnnotation("goodbye")
 * public class MyClass2{
 *  // ...
 * }
 * 
 * &#64;MyAnnotation(other="universe")
 * public class MyClass3{
 *  // ...
 * }
 * 
 * {@link AnnotationCache} cache = AnnotationCache.forAnnotationType(MyAnnotation.class);<br />
 * AnnotationValueRetrievalStrategy<MyAnnotation, String> strategy1 =
 *     SingleAnnotationValueRetrievalStrategy
 *         .forAnnotation(MyAnnotation.class, String.class); // use "value" method
 *         
 * AnnotationValueRetrievalStrategy<MyAnnotation, String> strategy2 =
 *     SingleAnnotationValueRetrievalStrategy
 *         .forAnnotation(MyAnnotation.class, String.class, "other");
 *
 * MyAnnotation annotation1 = cache.getAnnotationForType(MyClass1.class);
 * strategy1.retrieveAnnotationValue(annotation1); // hello
 * strategy2.retrieveAnnotationValue(annotation1); // world
 * 
 * MyAnnotation annotation2 = cache.getAnnotationForType(MyClass2.class);
 * strategy1.retrieveAnnotationValue(annotation2); // goodbye
 * strategy2.retrieveAnnotationValue(annotation2); // world
 * 
 * MyAnnotation annotation3 = cache.getAnnotationForType(MyClass3.class);
 * strategy1.retrieveAnnotationValue(annotation3); // hello
 * strategy2.retrieveAnnotationValue(annotation3); // universe
 * 
 * </pre></code>
 * 
 * @param <A>
 *            the annotation type
 * @param <T>
 *            the return type
 */
public class SingleAnnotationValueRetrievalStrategy<A extends Annotation, T>
        implements AnnotationValueRetrievalStrategy<A, T> {

    /**
     * Factory method to create a strategy based on the supplied annotation and
     * return types using the "value" property of the annotation.
     * 
     * @param <A>
     * @param <T>
     * @param annotationClass
     * @param returnType
     * @return the resolver
     */
    public static <A extends Annotation, T> SingleAnnotationValueRetrievalStrategy<A, T> forAnnotation(
            final Class<A> annotationClass, final Class<T> returnType) {
        return new SingleAnnotationValueRetrievalStrategy<A, T>(
                annotationClass, returnType);
    }

    /**
     * Factory method to create a strategy based on the supplied annotation and
     * return types using the supplied property of the annotation.
     * 
     * @param <A>
     * @param <T>
     * @param annotationClass
     * @param returnType
     * @param methodName
     * @return the resolver
     */
    public static <A extends Annotation, T> SingleAnnotationValueRetrievalStrategy<A, T> forAnnotation(
            final Class<A> annotationClass, final Class<T> returnType,
            final String methodName) {
        return new SingleAnnotationValueRetrievalStrategy<A, T>(
                annotationClass, returnType, methodName);
    }

    private Method valueMethod;

    /**
     * Protected constructor: use factory method
     * {@link #forAnnotation(Class, Class)} or write a subclass.
     * 
     * @param annotationClass
     * @param returnType
     */
    protected SingleAnnotationValueRetrievalStrategy(
            final Class<A> annotationClass, final Class<T> returnType) {
        this(annotationClass, returnType, "value");
    }

    /**
     * Protected constructor: use factory method
     * {@link #forAnnotation(Class, Class, String)} or write a subclass.
     * 
     * @param annotationClass
     * @param returnType
     * @param methodName
     */
    protected SingleAnnotationValueRetrievalStrategy(
            final Class<A> annotationClass, final Class<T> returnType,
            final String methodName) {
        try {
            this.valueMethod = annotationClass.getMethod(methodName);
            if (!returnType.isAssignableFrom(this.valueMethod.getReturnType())) {
                throw new IllegalArgumentException(
                        MessageFormat
                                .format(
                                        "Return type {0} is not compatible with requested return type {1}",
                                        this.valueMethod.getReturnType(),
                                        returnType));
            }
        } catch (final SecurityException e) {
            throw new IllegalArgumentException(MessageFormat.format(
                    "Can''t access the ''{1}'' method of {0}", annotationClass,
                    methodName), e);
        } catch (final NoSuchMethodException e) {
            throw new IllegalArgumentException(MessageFormat.format(
                    "Class {0} doesn''t have a ''{2}'' method: {1}",
                    annotationClass, annotationClass, methodName), e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public T retrieveAnnotationValue(final A annotation) {
        try {

            return annotation == null ? null : (T) this.valueMethod
                    .invoke(annotation);
        } catch (final IllegalAccessException e) {
            throw new IllegalStateException(MessageFormat.format(
                    "Can''t access property method {0} in annotation {1}",
                    this.valueMethod, annotation), e);
        } catch (final InvocationTargetException e) {
            throw new IllegalStateException(MessageFormat.format(
                    "Can''t access property method {0} in annotation {1}",
                    this.valueMethod, annotation), e);
        }
    }

}
